package org.jetbrains.idea.accurev.application;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.peer.PeerFactory;
import com.intellij.util.Processor;
import com.intellij.util.text.FilePathHashingStrategy;
import com.intellij.vcsUtil.VcsUtil;
import gnu.trove.THashSet;
import gnu.trove.TObjectLongHashMap;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.accurev.accurev.*;
import org.jetbrains.idea.accurev.accurev.connections.P4Connection;
import org.jetbrains.idea.accurev.accurev.connections.AccuRevConnectionManager;

import java.io.File;
import java.util.*;

/**
 * @author max
 */
public class PerforceChangeProvider implements ChangeProvider {
  private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.accurev.application.PerforceChangeProvider");

  private final Project myProject;
  private final AccuRevRunner myRunner;
  private LastSuccessfulUpdateTracker myLastSuccessfulUpdateTracker;

  public PerforceChangeProvider(final AccuRevVcs vcs) {
    myProject = vcs.getProject();
    myRunner = AccuRevRunner.getInstance(myProject);
    myLastSuccessfulUpdateTracker = LastSuccessfulUpdateTracker.getInstance(myProject);
  }

  public void getChanges(final VcsDirtyScope dirtyScope, final ChangelistBuilder builder, final ProgressIndicator progress) throws VcsException {
    LOG.debug("getting changes for scope " + dirtyScope);

    myLastSuccessfulUpdateTracker.updateStarted();
    if (dirtyScope.getRecursivelyDirtyDirectories().size() == 0) {
      final Set<FilePath> filePaths = dirtyScope.getDirtyFiles();
      boolean hasDirectories = false;
      for(FilePath filePath: filePaths) {
        if (filePath.isDirectory()) {
          hasDirectories = true;
          break;
        }
      }
      if (!hasDirectories) {
        getChangesForFiles(filePaths, builder, progress);
        myLastSuccessfulUpdateTracker.updateSuccessful();
        return;
      }
    }

    final TObjectLongHashMap<String> filesInPerforce = new TObjectLongHashMap<String>(FilePathHashingStrategy.create());
    Set<String> reportedChanges = new THashSet<String>(FilePathHashingStrategy.create());

    Set<P4Connection> connections = new THashSet<P4Connection>();
    final Collection<VirtualFile> roots = dirtyScope.getAffectedContentRoots();
    for (VirtualFile root : roots) {
      final P4Connection connection = AccuRevConnectionManager.getInstance(myProject).getConnectionForFile(root);
      if (connection != P4Connection.INVALID) {
        connections.add(connection);
      }
    }

    for (P4Connection connection : connections) {
      processConnection(connection, dirtyScope, builder, filesInPerforce, reportedChanges, progress);
    }

    if (builder.isUpdatingUnversionedFiles()) {
      final Set<String> localFiles = new THashSet<String>(FilePathHashingStrategy.create());
      final Set<String> writableFiles = new THashSet<String>(FilePathHashingStrategy.create());
      ApplicationManager.getApplication().runReadAction(new Runnable() {
        public void run() {
          dirtyScope.iterate(new Processor<FilePath>() {
            public boolean process(FilePath fileOrDir) {
              if (!fileOrDir.isDirectory()) {
                localFiles.add(FileUtil.toSystemDependentName(fileOrDir.getPath()));
                VirtualFile vf = fileOrDir.getVirtualFile();
                if (vf != null && vf.isWritable()) {
                  writableFiles.add(FileUtil.toSystemDependentName(fileOrDir.getPath()));
                }
              }
              return true;
            }
          });
        }
      });

      final Object[] keys = filesInPerforce.keys();
      final Set<String> unversionedFiles = new THashSet<String>(localFiles, FilePathHashingStrategy.create());
      for(Object key: keys) {
        unversionedFiles.remove((String) key);
      }
      unversionedFiles.removeAll(reportedChanges);

      final Set<String> missingFiles = new THashSet<String>(FilePathHashingStrategy.create());
      for(Object key: keys) {
        String fileName = (String) key;
        if (!localFiles.contains(fileName) && !reportedChanges.contains(fileName)) {
          missingFiles.add(fileName);
        }
      }

      writableFiles.removeAll(reportedChanges);
      writableFiles.removeAll(unversionedFiles);

      final LocalFileSystem fs = LocalFileSystem.getInstance();
      ApplicationManager.getApplication().runReadAction(new Runnable() {
        public void run() {
          for (String path : unversionedFiles) {
            // support Apple fork (IDEADEV-19059)
            int pos = path.lastIndexOf(File.separatorChar);
            if (pos < path.length()-1 && path.charAt(pos+1) == '%') {
              String basePath = path.substring(0, pos+1) + path.substring(pos+2);
              if (filesInPerforce.containsKey(basePath)) {
                continue;
              }
            }
            final VirtualFile file = fs.findFileByPath(FileUtil.toSystemIndependentName(path));
            if (file != null) {
              builder.processUnversionedFile(file);
            }
          }
          for(String path: writableFiles) {
            final VirtualFile file = fs.findFileByPath(FileUtil.toSystemIndependentName(path));
            if (file != null) {
              processWritableFile(file, builder);
            }
          }
          for (String path : missingFiles) {
            builder.processLocallyDeletedFile(VcsUtil.getFilePathForDeletedFile(path, false));
          }
        }
      });
    }
    myLastSuccessfulUpdateTracker.updateSuccessful();
  }

  private void processWritableFile(final VirtualFile file, final ChangelistBuilder builder) {
    boolean asyncEdit = false;
    if (AccuRevVcs.getInstance(myProject).isAsyncEditFile(file)) {
      try {
        long revision = myRunner.haveRevision(P4File.create(file));
        if (revision > 0) {
          asyncEdit = true;
          final FilePath filePath = PeerFactory.getInstance().getVcsContextFactory().createFilePathOn(file);
          builder.processChange(createEditedFileChange(filePath, revision, false));
        }
      }
      catch (VcsException e) {
        asyncEdit = false;
      }
    }
    if (!asyncEdit) {
      builder.processModifiedWithoutCheckout(file);
    }
  }

  private void getChangesForFiles(final Set<FilePath> dirtyFiles, final ChangelistBuilder builder, final ProgressIndicator progress) throws VcsException {
    Map<P4Connection, List<VirtualFile>> conflictMap = new HashMap<P4Connection, List<VirtualFile>>();
    List<ResolvedFile> resolvedFiles = null;
    TObjectLongHashMap<String> fileToRevisionMap = new TObjectLongHashMap<String>(FilePathHashingStrategy.create());
    for(final FilePath filePath: dirtyFiles) {
      progress.checkCanceled();
      if (filePath.isDirectory()) continue;
      P4Connection connection = ApplicationManager.getApplication().runReadAction(new Computable<P4Connection>() {
        @Nullable
        public P4Connection compute() {
          return AccuRevConnectionManager.getInstance(myProject).getConnectionForFile(filePath.getIOFile());
        }
      });
      P4File file = P4File.create(filePath);
      final Ref<Boolean> refWritable = new Ref<Boolean>();
      final VirtualFile virtualFile = ApplicationManager.getApplication().runReadAction(new Computable<VirtualFile>() {
        @Nullable
        public VirtualFile compute() {
          final VirtualFile vf = filePath.getVirtualFile();
          if (vf != null && vf.isWritable()) {
            refWritable.set(true);
          }
          return vf;
        }
      });

      PerforceChange change = myRunner.opened(file);
      boolean fileExists = filePath.getIOFile().exists();
      if (change == null) {
        boolean haveFile = myRunner.have(file);
        if (!haveFile && fileExists) {
          // support Apple fork (IDEADEV-19059)
          if (filePath.getName().startsWith("%")) {
            final FilePath parentPath = filePath.getParentPath();
            if (parentPath != null) {
              File baseFile = new File(parentPath.getIOFile(), filePath.getName().substring(1));
              if (myRunner.have(P4File.create(baseFile))) {
                haveFile = true;
              }
            }
          }
          if (!haveFile) {
            builder.processUnversionedFile(virtualFile);
          }
        }
        else if (virtualFile != null && !refWritable.isNull()) {
          processWritableFile(virtualFile, builder);
        }
        else if (haveFile && !fileExists) {
          builder.processLocallyDeletedFile(filePath);
        }
      }
      else {
        if (!fileExists && change.getType() != PerforceAbstractChange.DELETE) {
          builder.processLocallyDeletedFile(filePath);
        }
        else {
          List<VirtualFile> resolvedWithConflicts = conflictMap.get(connection);
          if (resolvedWithConflicts == null) {
            resolvedWithConflicts = myRunner.getResolvedWithConflicts(connection);
            conflictMap.put(connection, resolvedWithConflicts);
          }

          final AccuRevChangeListSynchronizer accuRevChangeListSynchronizer = AccuRevChangeListSynchronizer.getInstance(myProject);
          ChangeList changeList = accuRevChangeListSynchronizer.findChangeList(connection, change.getChangeList());
          if (changeList == null) {
            PerforceChangeList p4ChangeList = myRunner.describe(connection, change.getChangeList());
            changeList = accuRevChangeListSynchronizer.createChangeListByDescription(connection, p4ChangeList.getComment(), change.getChangeList());
          }

          if (change.getType() == PerforceAbstractChange.ADD || change.getType() == PerforceAbstractChange.BRANCH) {
            if (resolvedFiles == null) {
              resolvedFiles = myRunner.getResolvedFiles(connection);
            }
            boolean isMoved = false;
            for(ResolvedFile resolvedFile: resolvedFiles) {
              if (resolvedFile.getLocalFile().equals(filePath.getIOFile()) && resolvedFile.getOperation().equals(ResolvedFile.OPERATION_BRANCH)) {
                final PerforceClient client = AccuRevManager.getInstance(myProject).getClient(connection);
                final File branchSourceFile = AccuRevManager.getFileByDepotName(resolvedFile.getDepotPath(), client);
                if (branchSourceFile != null) {
                  final PerforceChange branchChange = myRunner.opened(P4File.create(branchSourceFile));
                  if (branchChange != null && branchChange.getType() == PerforceAbstractChange.DELETE) {
                    builder.processChangeInList(createRenameChange(connection, resolvedFile, filePath), changeList);
                    isMoved = true;
                  }
                }
                break;
              }
            }
            if (!isMoved) {
              builder.processChangeInList(createAddedFileChange(filePath, resolvedWithConflicts.contains(virtualFile)), changeList);
            }
          }
          else if (change.getType() == PerforceAbstractChange.EDIT || change.getType() == PerforceAbstractChange.INTEGRATE) {
            myRunner.have(file, connection, false, fileToRevisionMap);
            long haveRevision = fileToRevisionMap.get(filePath.getIOFile().getPath());
            builder.processChangeInList(createEditedFileChange(filePath, haveRevision, resolvedWithConflicts.contains(virtualFile)), changeList);
          }
          else if (change.getType() == PerforceAbstractChange.DELETE) {
            if (resolvedFiles == null) {
              resolvedFiles = myRunner.getResolvedFiles(connection);
            }

            boolean isMoved = false;
            for(ResolvedFile resolvedFile: resolvedFiles) {
              if (resolvedFile.getDepotPath().equals(change.getDepotPath())) {
                isMoved = true;
                break;
              }
            }
            if (!isMoved) {
              myRunner.have(file, connection, false, fileToRevisionMap);
              long haveRevision = fileToRevisionMap.get(filePath.getIOFile().getPath());
              builder.processChangeInList(createDeletedFileChange(filePath, haveRevision, resolvedWithConflicts.contains(virtualFile)), changeList);
            }
          }
        }
      }
    }
  }

  private void processConnection(final P4Connection connection,
                                 final VcsDirtyScope dirtyScope,
                                 final ChangelistBuilder builder,
                                 final TObjectLongHashMap<String> filesInPerforce,
                                 final Set<String> reportedChanges,
                                 final ProgressIndicator progress) throws VcsException {
    progress.checkCanceled();
    collectFilesInPerforce(filesInPerforce, dirtyScope, connection);
    progress.checkCanceled();

    final Set<VirtualFile> resolvedWithConflicts = new HashSet<VirtualFile>(myRunner.getResolvedWithConflicts(connection));
    List<ResolvedFile> resolvedFiles = null;

    final List<PerforceChange> abstractChanges = myRunner.getChangesForConnection(connection);
    progress.checkCanceled();
    final AccuRevChangeListSynchronizer accuRevChangeListSynchronizer = AccuRevChangeListSynchronizer.getInstance(myProject);
    // to process renames correctly, we need to process adds before renames
    Collections.sort(abstractChanges, new Comparator<PerforceChange>() {
      public int compare(final PerforceChange o1, final PerforceChange o2) {
        return o1.getType() - o2.getType();
      }
    });

    Set<PerforceChange> changesToSkip = new HashSet<PerforceChange>();
    for (PerforceChange perforceChange : abstractChanges) {
      LOG.debug("retrieved change " + perforceChange);
      if (changesToSkip.contains(perforceChange)) {
        continue;
      }

      final File file = perforceChange.getFile();
      final FilePath path = PeerFactory.getInstance().getVcsContextFactory().createFilePathOn(file);
      if (path == null || !dirtyScope.belongsTo(path)) {
        continue;
      }

      Change change = null;
      if (perforceChange.getType() == PerforceChange.BRANCH || perforceChange.getType() == PerforceChange.ADD) {
        if (resolvedFiles == null) {
          resolvedFiles = myRunner.getResolvedFiles(connection);
          progress.checkCanceled();
        }
        for(ResolvedFile resolvedFile: resolvedFiles) {
          if (resolvedFile.getLocalFile().equals(file) && resolvedFile.getOperation().equals(ResolvedFile.OPERATION_BRANCH)) {
            for(PerforceChange change2: abstractChanges) {
              if (change2.getType() == PerforceChange.DELETE && change2.getDepotPath().equals(resolvedFile.getDepotPath())) {
                changesToSkip.add(change2);
                change = createRenameChange(connection, resolvedFile, path);
                break;
              }
            }
            break;
          }
        }
      }

      if (change == null) {
        change = createChange(path, perforceChange, resolvedWithConflicts, filesInPerforce.get(file.getPath()));
      }
      if (change != null) {
        if (change.getType() == Change.Type.MODIFICATION) {
          ContentRevision revision = change.getAfterRevision();
          if (revision != null && !revision.getFile().getIOFile().exists()) {
            // let the file be reported as locally deleted (IDEADEV-10306)
            continue;
          }
        }

        long changeListNumber = perforceChange.getChangeList();
        String changeListDescription = perforceChange.getChangeListDescription();
        ChangeList changeList = accuRevChangeListSynchronizer.findOrCreateChangeList(connection, changeListNumber, changeListDescription);
        LOG.debug("processing in list " + changeList);
        builder.processChangeInList(change, changeList);
        final ContentRevision afterRevision = change.getAfterRevision();
        if (afterRevision != null) {
          reportedChanges.add(FileUtil.toSystemDependentName(afterRevision.getFile().getPath()));
        }
        final ContentRevision beforeRevision = change.getBeforeRevision();
        if (beforeRevision != null) {
          reportedChanges.add(FileUtil.toSystemDependentName(beforeRevision.getFile().getPath()));
        }
      }
    }
  }

  public boolean isModifiedDocumentTrackingRequired() {
    return false;
  }

  private void collectFilesInPerforce(TObjectLongHashMap<String> paths, VcsDirtyScope scope, P4Connection connection) throws VcsException {
    for (FilePath root : scope.getRecursivelyDirtyDirectories()) {
      myRunner.have(P4File.create(root), connection, true, paths);
    }

    for (FilePath path : scope.getDirtyFilesNoExpand()) {
      myRunner.have(P4File.create(path), connection, false, paths);
    }
  }

  @Nullable
  private Change createChange(final FilePath path, PerforceAbstractChange perforceChange,
                              final Collection<VirtualFile> resolvedWithConflicts, long haveRevision) {
    VirtualFile file = path.getVirtualFile();
    boolean isResolvedWithConflict = resolvedWithConflicts.contains(file);

    if (perforceChange.getType() == PerforceAbstractChange.ADD || perforceChange.getType() == PerforceAbstractChange.BRANCH) {
      return createAddedFileChange(path, isResolvedWithConflict);
    }
    else if (perforceChange.getType() == PerforceAbstractChange.DELETE) {
      return createDeletedFileChange(path, haveRevision, isResolvedWithConflict);
    }
    else if (perforceChange.getType() == PerforceAbstractChange.EDIT || perforceChange.getType() == PerforceAbstractChange.INTEGRATE) {
      return createEditedFileChange(path, haveRevision, isResolvedWithConflict);
    }

    //TODO: UNKNOWN
    return null;
  }

  private Change createEditedFileChange(final FilePath path, final long haveRevision, boolean isResolvedWithConflict) {
    return new Change(PerforceCachingContentRevision.create(myProject, path, haveRevision),
                      CurrentContentRevision.create(path),
                      isResolvedWithConflict ? FileStatus.MERGE : FileStatus.MODIFIED);
  }

  private static Change createAddedFileChange(final FilePath path, boolean isResolvedWithConflict) {
    return new Change(null, CurrentContentRevision.create(path),
                      isResolvedWithConflict ? FileStatus.MERGE : FileStatus.ADDED);
  }

  private Change createDeletedFileChange(final FilePath path, final long haveRevision, boolean isResolvedWithConflict) {
    return new Change(PerforceCachingContentRevision.create(myProject, path, haveRevision), null,
                      isResolvedWithConflict ? FileStatus.MERGE : FileStatus.DELETED);
  }

  private Change createRenameChange(final P4Connection connection, final ResolvedFile resolvedFile, final FilePath afterPath) {
    long revision = resolvedFile.getRevision2();
    if (revision < 0) {
      revision = resolvedFile.getRevision1();
    }
    ContentRevision beforeRevision = PerforceContentRevision.create(myProject, connection, resolvedFile.getDepotPath(), revision);
    ContentRevision afterRevision = CurrentContentRevision.create(afterPath);
    return new Change(beforeRevision, afterRevision, FileStatus.MODIFIED);
  }
}
